package creation_patterns.singleton_pattern.dcl;

import java.util.Objects;

/*
 * @Author xuhj
 * @Description synchronized同步块里面能够保证只创建一个对象。但是通过在synchronized的外面增加一层判断，就可以在对象一经创建以后，不再进入synchronized同步块。这种方案不仅减小了锁的粒度，保证了线程安全，性能方面也得到了大幅提升。volatile一般用于多线程的可见性，但是这里是用来防止指令重排序的。
 * @Date 10:32 下午 2022/1/1
 * @Param 
 * @return 
 **/
public class DclSingleton {
    private volatile static DclSingleton dclSingleton;
    private DclSingleton(){}
    public static DclSingleton getInstance(){
        // 第一个检查是否需要阻塞 因为出现锁竞争的情况还是少数，所以先加一个判断，避免多个线程同时等待这个锁
        if(Objects.isNull(dclSingleton)){
            synchronized (DclSingleton.class){
                // 第二个检查是否需要创建新的对象
                if(Objects.isNull(dclSingleton)){
                    // 该种方式会有指令重排的问题 因此需要加volatile关键字
                    dclSingleton = new DclSingleton();
                }
            }
        }
        return dclSingleton;
    }
}

/**
 * 为什么需要volatile？volatile有什么用？
 *
 * 首先要回答可见性，这个是毋庸质疑的，然后可能又会考到java内存模型。
 * 防止指令重排序: 防止new DclSingleton()时指令重排序导致其他线程获取到未初始化完的对象。instance = new Singleton()这句，这并非是一个原子操作，事实上在 JVM 中这句话大概做了下面 3 件事情。1.给 instance 分配内存2.调用 Singleton 的构造函数来初始化成员变量3.将instance对象指向分配的内存空间（执行完这步 instance 就为非 null 了）
 * 但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的，最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者，则在 3 执行完毕、2 未执行之前，被线程二抢占了，这时 instance 已经是非 null 了（但却没有初始化），所以线程二会直接返回 instance，然后使用，然后报错。
 * 顺便也可以说下volatie原理用内存屏障
 *
 * 讲讲synchronized和volatile的区别
 * 这里可以从synchroized能保证原子性，volatile不能保证，以及讲下synchroized是重量级锁，甚至可以所以下他和Lock的区别等等。
 * 线程安全一般怎么实现的?
 *
 * 互斥同步。如lock,synchroized
 * 非阻塞同步。如cas。
 * 不同步。如threadLocal,局部变量
 * */